﻿Imports System.Runtime.CompilerServices
Imports System.Data.Common
Imports System.Linq.Expressions
Imports System.Reflection
Imports System.Text
Imports Kairyu.Extension

Namespace Load

    Public Module QueryExtension

#Region "Equal"
        <Extension()>
        Public Function WhereEquals(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return WhereEquals(source, DirectCast(e.Body, MemberExpression).Member, val)
        End Function

        <Extension()>
        Friend Function WhereEquals(Of TObj)(source As IQuery(Of TObj), member As MemberInfo, val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} = {1}", member, val)
        End Function

        <Extension()>
        Public Function WhereNotEquals(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} <> {1}", e, val)
        End Function
#End Region

#Region "In"
        <Extension()>
        Public Function WhereIn(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), vals() As Object) As IQuery(Of TObj)
            Return Where_MultiValue(source, "{0} IN ({1})", e, vals)
        End Function

        <Extension()>
        Friend Function WhereIn(Of TObj)(source As IQuery(Of TObj), member As MemberInfo, vals() As Object) As IQuery(Of TObj)
            Return Where_MultiValue(source, "{0} IN ({1})", member, vals)
        End Function

        <Extension()>
        Public Function WhereNotIn(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), vals() As Object) As IQuery(Of TObj)
            Return Where_MultiValue(source, "{0} NOT IN ({1})", e, vals)

        End Function

        <Extension()>
        Friend Function WhereInOrEqual(Of TObj)(source As IQuery(Of TObj), member As MemberInfo, vals() As Object) As IQuery(Of TObj)
            If vals.Count = 1 Then
                Return WhereEquals(source, member, vals.First)
            Else
                Return WhereIn(source, member, vals)
            End If
        End Function
#End Region

#Region "Less Than, Greater Than"
        <Extension()> _
        Public Function WhereLess(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} < {1}", e, val)
        End Function

        <Extension()> _
        Public Function WhereLessOrEqual(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} <= {1}", e, val)
        End Function

        <Extension()> _
        Public Function WhereGreater(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{1} < {0}", e, val)
        End Function

        <Extension()> _
        Public Function WhereGreaterOrEqual(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{1} <= {0}", e, val)
        End Function
#End Region

#Region "Like"
        <Extension()> _
        Public Function WhereLike(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} LIKE {1}", e, val)
        End Function

        <Extension()> _
        Public Function WhereNotLike(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, "{0} NOT LIKE {1}", e, val)
        End Function
#End Region

#Region "Between"
        <Extension()> _
        Public Function WhereBetween(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), sval As Object, eval As Object) As IQuery(Of TObj)
            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
                Sub(nameReader, prmRegister, whereRegister)
                    Dim colName As String = nameReader.Invoke(DirectCast(e.Body, MemberExpression).Member)
                    Dim prmS As String = prmRegister.Invoke(sval)
                    Dim prmE As String = prmRegister.Invoke(eval)
                    Dim s As String = String.Format("{0} BETWEEN {1} AND {2}", colName, prmS, prmE)
                    whereRegister.Invoke(s)
                End Sub

            source.ParameterBuilders.Add(builder)
            Return source
        End Function
#End Region

#Region "Core"
        Private Function Where_SingleValue(Of TObj, TProp)(source As IQuery(Of TObj), format As String, e As Expression(Of Func(Of TObj, TProp)), val As Object) As IQuery(Of TObj)
            Return Where_SingleValue(source, format, DirectCast(e.Body, MemberExpression).Member, val)
        End Function

        Friend Function Where_SingleValue(Of TObj)(source As IQuery(Of TObj), format As String, member As MemberInfo, val As Object) As IQuery(Of TObj)
            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
                Sub(nameReader, prmRegister, whereRegister)
                    Dim colName As String = nameReader.Invoke(member)
                    Dim prmName As String = prmRegister.Invoke(val)
                    Dim s As String = String.Format(format, colName, prmName)
                    whereRegister.Invoke(s)
                End Sub

            source.ParameterBuilders.Add(builder)
            Return source
        End Function

        Private Function Where_MultiValue(Of TObj, TProp)(source As IQuery(Of TObj), format As String, e As Expression(Of Func(Of TObj, TProp)), vals() As Object) As IQuery(Of TObj)
            Return Where_MultiValue(source, format, DirectCast(e.Body, MemberExpression).Member, vals)
        End Function

        Private Function Where_MultiValue(Of TObj)(source As IQuery(Of TObj), format As String, member As MemberInfo, vals() As Object) As IQuery(Of TObj)
            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
               Sub(nameReader, prmRegister, whereRegister)
                   Dim colName As String = nameReader.Invoke(member)
                   Dim txt As String = String.Empty
                   For Each item In vals
                       Dim prmName As String = prmRegister.Invoke(item)
                       If String.IsNullOrEmpty(txt) = False Then txt &= ", "
                       txt &= prmName
                   Next

                   Dim s As String = String.Format(format, colName, txt)
                   whereRegister.Invoke(s)
               End Sub

            source.ParameterBuilders.Add(builder)
            Return source
        End Function
#End Region

        ''' <summary>
        ''' OR条件式を設定
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <param name="queries">OR条件対象となる式</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function WhereOR(Of T)(source As IQuery(Of T), queries() As IQuery(Of T)) As IQuery(Of T)
            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
                Sub(nameReader, prmRegister, whereRegister)
                    '内部検索条件をビルド。検索文字列を仮受けしてORでつないだものをまとめて返す
                    Dim txt As New StringBuilder
                    Dim isOne As Boolean = If(queries.Length = 1, True, False)
                    For Each item In queries
                        Dim innerTxt As New StringBuilder
                        Dim useAnd As Boolean = False

                        'クエリ内の条件はANDで結合
                        Dim innerWhereRegister As Action(Of String) =
                            Sub(s)
                                If innerTxt.Any Then
                                    innerTxt.Append(" AND ")
                                    useAnd = True
                                End If
                                innerTxt.Append(s)
                            End Sub
                        For Each b In item.ParameterBuilders
                            b.Invoke(nameReader, prmRegister, innerWhereRegister)
                        Next

                        'クエリ間の条件はORで結合
                        txt.AppendDelimiter(" OR ")
                        txt.Append(If(isOne = False AndAlso useAnd, String.Format("({0})", innerTxt), innerTxt))
                    Next
                    If isOne = False Then
                        whereRegister.Invoke(String.Format("({0})", txt.ToString))
                    Else
                        whereRegister.Invoke(txt.ToString)
                    End If

                End Sub

            source.ParameterBuilders.Add(builder)
            Return source
        End Function

        ''' <summary>
        ''' クエリ処理を実行し、追加処理可能なリストを取得します。
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function Execute(Of T)(source As IQuery(Of T)) As QueryableList(Of T)
            'クエリを強制的に実行し、型を変換します。
            Return New QueryableList(Of T)(source.Provider.Loader, source.ToList)
        End Function

        <Extension()>
        Public Function Paging(Of TObj, TProp)(source As IQuery(Of TObj), e As Expression(Of Func(Of TObj, TProp)), pageNo As Integer, pageSize As Integer) As QueryableList(Of TObj)
            Dim sort() = {DirectCast(DirectCast(e.Body, MemberExpression).Member, PropertyInfo)}
            source.Provider = New PagingQueryProvider(source.Provider, sort, pageNo, pageSize)

            Dim lst As PageList(Of TObj) = source.Provider.Execute(source.Expression)
            Return New QueryableList(Of TObj)(source.Provider.Loader, lst)
        End Function

#Region "KeyLoad"
        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), id As String) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsPrimaryKey, id)
        End Function
        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), id As Integer) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsPrimaryKey, id)
        End Function
        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), id As Long) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsPrimaryKey, id)
        End Function

        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), ids() As String) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsPrimaryKey, ids)
        End Function
        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), ids() As Integer) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsPrimaryKey, ids)
        End Function
        <Extension()>
        Public Function WhereKeyEquals(Of T)(source As Query(Of T), ids() As Long) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsPrimaryKey, ids)
        End Function

        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), id As String) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsAlternativeLoadKey, id)
        End Function
        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), id As Integer) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsAlternativeLoadKey, id)
        End Function
        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), id As Long) As Query(Of T)
            Return WhereKey_SingleValue(source, Function(x) x.IsAlternativeLoadKey, id)
        End Function

        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), ids() As String) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsAlternativeLoadKey, ids)
        End Function
        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), ids() As Integer) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsAlternativeLoadKey, ids)
        End Function
        <Extension()>
        Public Function WhereAlternativeKeyEquals(Of T)(source As Query(Of T), ids() As Long) As Query(Of T)
            Return WhereKey_MultiValue(source, Function(x) x.IsAlternativeLoadKey, ids)
        End Function

        Private Function WhereKey_SingleValue(Of T)(source As Query(Of T), target As Predicate(Of ColumnAttribute), id As Object) As Query(Of T)
            Dim cols = (From el In GetType(T).ToTable.Columns Where target.Invoke(el)).ToList
#If DEBUG Then
            If cols.Count <> 1 Then Throw New NotSupportedException
#End If

            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
                 Sub(nameReader, prmRegister, whereRegister)
                     Dim colName As String = nameReader.Invoke(cols.First.PropertyInfo)
                     Dim prmName As String = prmRegister.Invoke(id)
                     Dim s As String = String.Format("{0} = {1}", colName, prmName)
                     whereRegister.Invoke(s)
                 End Sub

            source.Builders.Add(builder)
            Return source
        End Function

        Private Function WhereKey_MultiValue(Of T)(source As Query(Of T), target As Predicate(Of ColumnAttribute), ids As IList) As Query(Of T)
            Dim cols = (From el In GetType(T).ToTable.Columns Where target.Invoke(el)).ToList
#If DEBUG Then
            If cols.Count <> 1 Then Throw New NotSupportedException
#End If

            Dim builder As Action(Of Func(Of MemberInfo, String), System.Func(Of Object, String), System.Action(Of String)) =
                Sub(nameReader, prmRegister, whereRegister)
                    Dim colName As String = nameReader.Invoke(cols.First.PropertyInfo)
                    Dim txt As New StringBuilder
                    For Each item In ids
                        Dim prmName As String = prmRegister.Invoke(item)
                        txt.AppendDelimiter(", ").Append(prmName)
                    Next
                    Dim s As String = String.Format("{0} IN ({1})", colName, txt.ToString)
                    whereRegister.Invoke(s)
                End Sub

            source.Builders.Add(builder)
            Return source
        End Function
#End Region

#Region "WhereReLoad"
        ''' <summary>
        ''' インスタンスからリロード検索条件を設定
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <param name="m">検索条件インスタンス</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function WhereReLoad(Of T)(source As Query(Of T), m As T) As Query(Of T)
            Return WhereReLoad(source, {m})
        End Function

        ''' <summary>
        ''' インスタンスからリロード検索条件を設定
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <param name="ms">検索条件インスタンス</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function WhereReLoad(Of T)(source As Query(Of T), ms As IEnumerable(Of T)) As Query(Of T)
            Dim fn As Func(Of T, Query(Of T)) =
               Function(m)
                   Dim q = Query(Of T).CreateMockUp
                   'リロード検索条件対象列を取得
                   For Each item In (From el In GetType(T).ToTable.Columns Where el.IsRelaodKey)
                       q.WhereEquals(DirectCast(item.PropertyInfo, MemberInfo), item.PropertyInfo.GetValue(m, Nothing))
                   Next
                   Return q
               End Function

            Return source.WhereOR((From el In ms Select fn.Invoke(el)).ToArray)
        End Function
#End Region

#Region "WhereLoad"
        ''' <summary>
        ''' インスタンスから主キー検索条件を設定
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <param name="m">検索条件インスタンス</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function WhereLoad(Of T)(source As Query(Of T), m As T) As Query(Of T)
            Return WhereLoad(source, {m})
        End Function

        ''' <summary>
        ''' インスタンスから主キー検索条件を設定
        ''' </summary>
        ''' <typeparam name="T">自身の型</typeparam>
        ''' <param name="source">読み込み起点となるリスト</param>
        ''' <param name="ms">検索条件インスタンス</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        <Extension()>
        Public Function WhereLoad(Of T)(source As Query(Of T), ms As IEnumerable(Of T)) As Query(Of T)
            Dim fn As Func(Of T, Query(Of T)) =
               Function(m)
                   Dim q = Query(Of T).CreateMockUp
                   '主キー検索条件対象列を取得
                   For Each item As ColumnAttribute In (From el In GetType(T).ToTable.Columns Where el.IsPrimaryKey)
                       '#103 「親－子－孫の関係時、子、孫を起点とした読み込みができない」不具合修正
                       '列属性クラスから値を取得するように変更
                       Dim val As Object = item.GetValue(m)
                       Dim mem As MemberInfo = DirectCast(item.GetRelationRootColumn.PropertyInfo, MemberInfo)

                       q.WhereEquals(mem, val)
                   Next
                   Return q
               End Function

            Return source.WhereOR((From el In ms Select fn.Invoke(el)).ToArray)
        End Function
#End Region

        ''' <summary>
        ''' マッピング除外を設定
        ''' </summary>
        ''' <typeparam name="T"></typeparam>
        ''' <param name="source"></param>
        ''' <param name="prop"></param>
        ''' <returns></returns>
        ''' <remarks>主に子読み込み時に自身のマッピングを除外するのに使用</remarks>
        <Extension()>
        Public Function MappingIgnore(Of T)(source As Query(Of T), prop As PropertyInfo) As Query(Of T)
            source.Provider.Loader.MappingIgnoreProperty = prop
            Return source
        End Function

    End Module

End Namespace